package edu.northwestern.cbits.purple_robot_manager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.support.wearable.watchface.CanvasWatchFaceService;
import android.support.wearable.watchface.WatchFaceService;
import android.support.wearable.watchface.WatchFaceStyle;
import android.text.format.Time;
import android.view.SurfaceHolder;
import android.view.WindowInsets;
import java.util.TimeZone;
import java.util.concurrent.TimeUnit;
public class DigitalWatchFaceService extends CanvasWatchFaceService
{
private static final String TAG = "DigitalWatchFaceService";
private static final Typeface BOLD_TYPEFACE = Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD);
private static final Typeface NORMAL_TYPEFACE = Typeface.create(Typeface.SANS_SERIF, Typeface.NORMAL);
private static final long NORMAL_UPDATE_RATE_MS = 500;
private static final long MUTE_UPDATE_RATE_MS = TimeUnit.MINUTES.toMillis(1);
private static final long mWatchStart = System.currentTimeMillis();
@Override
public Engine onCreateEngine()
{
return new Engine();
}
private class Engine extends CanvasWatchFaceService.Engine
{
static final String COLON_STRING = ":";
static final int MUTE_ALPHA = 100;
static final int NORMAL_ALPHA = 255;
static final int MSG_UPDATE_TIME = 0;
long mInteractiveUpdateRateMs = NORMAL_UPDATE_RATE_MS;
final Handler mUpdateTimeHandler = new Handler()
{
@Override
public void handleMessage(Message message)
{
switch (message.what)
{
case MSG_UPDATE_TIME:
Engine.this.invalidate();
if (shouldTimerBeRunning())
{
long timeMs = System.currentTimeMillis();
long delayMs = mInteractiveUpdateRateMs - (timeMs % mInteractiveUpdateRateMs);
mUpdateTimeHandler.sendEmptyMessageDelayed(MSG_UPDATE_TIME, delayMs);
}
break;
}
Intent heartbeat = new Intent(DigitalWatchFaceService.this, HeartbeatService.class);
DigitalWatchFaceService.this.startService(heartbeat);
}
};
final BroadcastReceiver mTimeZoneReceiver = new BroadcastReceiver()
{
@Override
public void onReceive(Context context, Intent intent)
{
mTime.clear(intent.getStringExtra("time-zone"));
mTime.setToNow();
}
};
boolean mRegisteredTimeZoneReceiver = false;
Paint mBackgroundPaint;
Paint mHourPaint;
Paint mMinutePaint;
Paint mSecondPaint;
Paint mAmPmPaint;
Paint mColonPaint;
Paint mSamplesPaint;
float mColonWidth;
boolean mMute;
Time mTime;
boolean mShouldDrawColons;
float mXOffset;
float mYOffset;
String mAmString;
String mPmString;
int mInteractiveBackgroundColor = DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_BACKGROUND;
int mAmbientBackgroundColor = DigitalWatchFaceUtil.COLOR_VALUE_AMBIENT_BACKGROUND;
int mInteractiveHourDigitsColor = DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_HOUR_DIGITS;
int mInteractiveMinuteDigitsColor = DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_MINUTE_DIGITS;
int mInteractiveSecondDigitsColor = DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_SECOND_DIGITS;
/**
* Whether the display supports fewer bits for each color in ambient mode. When true, we
* disable anti-aliasing in ambient mode.
*/
boolean mLowBitAmbient;
@Override
public void onCreate(SurfaceHolder holder)
{
super.onCreate(holder);
this.setWatchFaceStyle(new WatchFaceStyle.Builder(DigitalWatchFaceService.this)
.setCardPeekMode(WatchFaceStyle.PEEK_MODE_VARIABLE)
.setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE)
.setShowSystemUiTime(false)
.build());
Resources resources = DigitalWatchFaceService.this.getResources();
mYOffset = resources.getDimension(R.dimen.digital_y_offset);
mAmString = resources.getString(R.string.digital_am);
mPmString = resources.getString(R.string.digital_pm);
mBackgroundPaint = new Paint();
mBackgroundPaint.setColor(mInteractiveBackgroundColor);
mHourPaint = createTextPaint(mInteractiveHourDigitsColor, BOLD_TYPEFACE);
mMinutePaint = createTextPaint(mInteractiveMinuteDigitsColor);
mSecondPaint = createTextPaint(mInteractiveSecondDigitsColor);
mAmPmPaint = createTextPaint(resources.getColor(R.color.digital_am_pm));
mColonPaint = createTextPaint(resources.getColor(R.color.digital_colons));
mSamplesPaint = createTextPaint(mInteractiveHourDigitsColor, NORMAL_TYPEFACE);
mTime = new Time();
}
@Override
public void onDestroy()
{
mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
super.onDestroy();
}
private Paint createTextPaint(int defaultInteractiveColor)
{
return createTextPaint(defaultInteractiveColor, NORMAL_TYPEFACE);
}
private Paint createTextPaint(int defaultInteractiveColor, Typeface typeface)
{
Paint paint = new Paint();
paint.setColor(defaultInteractiveColor);
paint.setTypeface(typeface);
paint.setAntiAlias(true);
return paint;
}
@Override
public void onVisibilityChanged(boolean visible)
{
super.onVisibilityChanged(visible);
if (visible)
{
registerReceiver();
// Update time zone in case it changed while we weren't visible.
mTime.clear(TimeZone.getDefault().getID());
mTime.setToNow();
}
else
unregisterReceiver();
updateTimer();
}
private void registerReceiver()
{
if (mRegisteredTimeZoneReceiver)
return;
mRegisteredTimeZoneReceiver = true;
IntentFilter filter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED);
DigitalWatchFaceService.this.registerReceiver(mTimeZoneReceiver, filter);
}
private void unregisterReceiver()
{
if (!mRegisteredTimeZoneReceiver)
return;
mRegisteredTimeZoneReceiver = false;
DigitalWatchFaceService.this.unregisterReceiver(mTimeZoneReceiver);
}
@Override
public void onApplyWindowInsets(WindowInsets insets)
{
super.onApplyWindowInsets(insets);
Resources resources = DigitalWatchFaceService.this.getResources();
boolean isRound = insets.isRound();
mXOffset = resources.getDimension(isRound ? R.dimen.digital_x_offset_round : R.dimen.digital_x_offset);
float textSize = resources.getDimension(isRound ? R.dimen.digital_text_size_round : R.dimen.digital_text_size);
float amPmSize = resources.getDimension(isRound ? R.dimen.digital_am_pm_size_round : R.dimen.digital_am_pm_size);
float samplesSize = resources.getDimension(isRound ? R.dimen.digital_samples_size_round : R.dimen.digital_samples_size);
mHourPaint.setTextSize(textSize);
mMinutePaint.setTextSize(textSize);
mSecondPaint.setTextSize(textSize);
mAmPmPaint.setTextSize(amPmSize);
mColonPaint.setTextSize(textSize);
mSamplesPaint.setTextSize(samplesSize);
mColonWidth = mColonPaint.measureText(COLON_STRING);
}
@Override
public void onPropertiesChanged(Bundle properties)
{
super.onPropertiesChanged(properties);
boolean burnInProtection = properties.getBoolean(PROPERTY_BURN_IN_PROTECTION, false);
mHourPaint.setTypeface(burnInProtection ? NORMAL_TYPEFACE : BOLD_TYPEFACE);
mLowBitAmbient = properties.getBoolean(PROPERTY_LOW_BIT_AMBIENT, false);
}
@Override
public void onTimeTick()
{
super.onTimeTick();
this.invalidate();
Intent heartbeat = new Intent(DigitalWatchFaceService.this, HeartbeatService.class);
DigitalWatchFaceService.this.startService(heartbeat);
}
@Override
public void onAmbientModeChanged(boolean inAmbientMode)
{
super.onAmbientModeChanged(inAmbientMode);
if (inAmbientMode)
adjustPaintColorToCurrentMode(mBackgroundPaint, mInteractiveBackgroundColor, DigitalWatchFaceUtil.COLOR_VALUE_AMBIENT_BACKGROUND);
else
adjustPaintColorToCurrentMode(mBackgroundPaint, mInteractiveBackgroundColor, DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_BACKGROUND);
adjustPaintColorToCurrentMode(mHourPaint, mInteractiveHourDigitsColor, DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_HOUR_DIGITS);
adjustPaintColorToCurrentMode(mMinutePaint, mInteractiveMinuteDigitsColor, DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_MINUTE_DIGITS);
adjustPaintColorToCurrentMode(mSamplesPaint, mInteractiveMinuteDigitsColor, DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_MINUTE_DIGITS);
// Actually, the seconds are not rendered in the ambient mode, so we could pass just any
// value as ambientColor here.
adjustPaintColorToCurrentMode(mSecondPaint, mInteractiveSecondDigitsColor, DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_SECOND_DIGITS);
if (mLowBitAmbient){
boolean antiAlias = !inAmbientMode;
mHourPaint.setAntiAlias(antiAlias);
mMinutePaint.setAntiAlias(antiAlias);
mSecondPaint.setAntiAlias(antiAlias);
mAmPmPaint.setAntiAlias(antiAlias);
mColonPaint.setAntiAlias(antiAlias);
mSamplesPaint.setAntiAlias(antiAlias);
}
this.invalidate();
Intent heartbeat = new Intent(DigitalWatchFaceService.this, HeartbeatService.class);
DigitalWatchFaceService.this.startService(heartbeat);
this.updateTimer();
}
private void adjustPaintColorToCurrentMode(Paint paint, int interactiveColor, int ambientColor)
{
paint.setColor(isInAmbientMode() ? ambientColor : interactiveColor);
}
@Override
public void onInterruptionFilterChanged(int interruptionFilter)
{
super.onInterruptionFilterChanged(interruptionFilter);
boolean inMuteMode = interruptionFilter == WatchFaceService.INTERRUPTION_FILTER_NONE;
setInteractiveUpdateRateMs(inMuteMode ? MUTE_UPDATE_RATE_MS : NORMAL_UPDATE_RATE_MS);
if (mMute != inMuteMode)
{
mMute = inMuteMode;
int alpha = inMuteMode ? MUTE_ALPHA : NORMAL_ALPHA;
mHourPaint.setAlpha(alpha);
mMinutePaint.setAlpha(alpha);
mColonPaint.setAlpha(alpha);
mAmPmPaint.setAlpha(alpha);
mSamplesPaint.setAlpha(alpha);
this.invalidate();
Intent heartbeat = new Intent(DigitalWatchFaceService.this, HeartbeatService.class);
DigitalWatchFaceService.this.startService(heartbeat);
}
}
public void setInteractiveUpdateRateMs(long updateRateMs)
{
if (updateRateMs == mInteractiveUpdateRateMs)
return;
mInteractiveUpdateRateMs = updateRateMs;
// Stop and restart the timer so the new update rate takes effect immediately.
if (shouldTimerBeRunning())
updateTimer();
}
private String formatTwoDigitNumber(int hour)
{
return String.format("%02d", hour);
}
private int convertTo12Hour(int hour)
{
int result = hour % 12;
return (result == 0) ? 12 : result;
}
private String getAmPmString(int hour)
{
return (hour < 12) ? mAmString : mPmString;
}
@Override
public void onDraw(Canvas canvas, Rect bounds)
{
mTime.setToNow();
// Show colons for the first half of each second so the colons blink on when the time
// updates.
mShouldDrawColons = (System.currentTimeMillis() % 1000) < 500;
// Draw the background.
canvas.drawRect(0, 0, bounds.width(), bounds.height(), mBackgroundPaint);
// Draw the hours.
float x = mXOffset;
String hourString = String.valueOf(convertTo12Hour(mTime.hour));
canvas.drawText(hourString, x, mYOffset, mHourPaint);
x += mHourPaint.measureText(hourString);
// In ambient and mute modes, always draw the first colon. Otherwise, draw the
// first colon for the first half of each second.
if (isInAmbientMode() || mMute || mShouldDrawColons) {
canvas.drawText(COLON_STRING, x, mYOffset, mColonPaint);
}
x += mColonWidth;
float samplesOffset = x;
// Draw the minutes.
String minuteString = formatTwoDigitNumber(mTime.minute);
canvas.drawText(minuteString, x, mYOffset, mMinutePaint);
x += mMinutePaint.measureText(minuteString);
// In ambient and mute modes, draw AM/PM. Otherwise, draw a second blinking
// colon followed by the seconds.
if (isInAmbientMode() || mMute)
{
x += mColonWidth;
canvas.drawText(getAmPmString(mTime.hour), x, mYOffset, mAmPmPaint);
}
else
{
if (mShouldDrawColons)
canvas.drawText(COLON_STRING, x, mYOffset, mColonPaint);
x += mColonWidth;
canvas.drawText(formatTwoDigitNumber(mTime.second), x, mYOffset, mSecondPaint);
}
Rect samplesRect = new Rect();
mSamplesPaint.getTextBounds("0", 0, 1, samplesRect);
int payloadsCount = SensorService.pendingPayloadsCount();
long now = System.currentTimeMillis();
double runtime = ((double) (now - mWatchStart)) / (60 * 1000);
int battery = SensorService.currentBatteryLevel();
String payloadsStatus = DigitalWatchFaceService.this.getString(R.string.label_payloads, payloadsCount, runtime, battery);
Resources resources = DigitalWatchFaceService.this.getResources();
float paddingSize = resources.getDimension(R.dimen.payload_padding_size);
canvas.drawText(payloadsStatus, samplesOffset, mYOffset + paddingSize + samplesRect.height(), mSamplesPaint);
}
/**
* Starts the {@link #mUpdateTimeHandler} timer if it should be running and isn't currently
* or stops it if it shouldn't be running but currently is.
*/
private void updateTimer()
{
mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
if (shouldTimerBeRunning())
mUpdateTimeHandler.sendEmptyMessage(MSG_UPDATE_TIME);
}
/**
* Returns whether the {@link #mUpdateTimeHandler} timer should be running. The timer should
* only run when we're visible and in interactive mode.
*/
private boolean shouldTimerBeRunning()
{
return isVisible() && !isInAmbientMode();
}
}
}